using System;

using Mono.Cecil.Metadata;

namespace Mono.Cecil {

	sealed class TypeSystem {

		abstract class TypeSystemLookup {

			protected ModuleDefinition module;

			protected TypeSystemLookup (ModuleDefinition module)
			{
				this.module = module;
			}

			public abstract TypeReference LookupType (string fullname);

			public static TypeSystemLookup CreateLookupFor (ModuleDefinition module)
			{
				if (IsCorlib (module))
					return new CorlibTypeSystemLookup (module);

				return new CommonTypeSystemLookup (module);
			}

			static bool IsCorlib (ModuleDefinition module)
			{
				if (module.Assembly == null)
					return false;

				return module.Assembly.Name.Name == Constants.Corlib;
			}
		}

		sealed class CorlibTypeSystemLookup : TypeSystemLookup {

			public CorlibTypeSystemLookup (ModuleDefinition module)
				: base (module)
			{
			}

			public override TypeReference LookupType (string fullname)
			{
				var types = module.MetadataSystem.Types;

				for (int i = 0; i < types.Length; i++) {
					var type = types [i];
					if (type == null || type.Namespace != "System")
						continue;

					if (type.FullName == fullname)
						return type;
				}

				return null;
			}
		}

		sealed class CommonTypeSystemLookup : TypeSystemLookup {

			AssemblyNameReference corlib;

			public CommonTypeSystemLookup (ModuleDefinition module)
				: base (module)
			{
			}

			public override TypeReference LookupType (string fullname)
			{
				return CreateTypeReference (fullname);
			}

			public AssemblyNameReference GetCorlibReference ()
			{
				if (corlib != null)
					return corlib;

				foreach (var reference in module.AssemblyReferences)
					if (reference.Name == Constants.Corlib)
						return corlib = reference;

				return corlib = new AssemblyNameReference {
					Name = Constants.Corlib,
					Version = GetCorlibVersion (),
					PublicKeyToken = new byte [] { 0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89 },
				};
			}

			Version GetCorlibVersion ()
			{
				switch (module.Runtime) {
				case TargetRuntime.Net_1_0:
				case TargetRuntime.Net_1_1:
					return new Version (1, 0);
				case TargetRuntime.Net_2_0:
					return new Version (2, 0);
				case TargetRuntime.Net_4_0:
					return new Version (4, 0);
				default:
					throw new NotSupportedException ();
				}
			}

			TypeReference CreateTypeReference (string fullname)
			{
				var parts = fullname.Split ('.');
				var type = new TypeReference (parts [1], parts [0], GetCorlibReference ());
				type.Module = module;
				return type;
			}
		}

		ModuleDefinition module;
		TypeSystemLookup lookup;

		TypeReference type_object;
		TypeReference type_void;
		TypeReference type_bool;
		TypeReference type_char;
		TypeReference type_sbyte;
		TypeReference type_byte;
		TypeReference type_int16;
		TypeReference type_uint16;
		TypeReference type_int32;
		TypeReference type_uint32;
		TypeReference type_int64;
		TypeReference type_uint64;
		TypeReference type_single;
		TypeReference type_double;
		TypeReference type_intptr;
		TypeReference type_uintptr;
		TypeReference type_string;
		TypeReference type_typedref;

		TypeSystemLookup Lookup {
			get {
				if (lookup == null)
					lookup = TypeSystemLookup.CreateLookupFor (module);

				return lookup;
			}
		}

		public TypeSystem (ModuleDefinition module)
		{
			this.module = module;
		}

		TypeReference LookupType (string fullname, ElementType element_type)
		{
			var type = Lookup.LookupType (fullname);
			type.etype = element_type;
			return type;
		}

		TypeReference LookupValueType (string fullname, ElementType element_type)
		{
			var type = Lookup.LookupType (fullname);
			type.IsValueType = true;
			type.etype = element_type;
			return type;
		}

		public AssemblyNameReference Corlib {
			get {
				var common = Lookup as CommonTypeSystemLookup;
				if (common == null)
					throw new NotSupportedException ();

				return common.GetCorlibReference ();
			}
		}

		public TypeReference Object {
			get {
				if (type_object == null)
					type_object = LookupType (Constants.Object, ElementType.Object);

				return type_object;
			}
		}

		public TypeReference Void {
			get {
				if (type_void == null)
					type_void = LookupType (Constants.Void, ElementType.Void);

				return type_void;
			}
		}

		public TypeReference Boolean {
			get {
				if (type_bool == null)
					type_bool = LookupValueType (Constants.Boolean, ElementType.Boolean);

				return type_bool;
			}
		}

		public TypeReference Char {
			get {
				if (type_char == null)
					type_char = LookupValueType (Constants.Char, ElementType.Char);

				return type_char;
			}
		}

		public TypeReference SByte {
			get {
				if (type_sbyte == null)
					type_sbyte = LookupValueType (Constants.SByte, ElementType.I1);

				return type_sbyte;
			}
		}

		public TypeReference Byte {
			get {
				if (type_byte == null)
					type_byte = LookupValueType (Constants.Byte, ElementType.U1);

				return type_byte;
			}
		}

		public TypeReference Int16 {
			get {
				if (type_int16 == null)
					type_int16 = LookupValueType (Constants.Int16, ElementType.I2);

				return type_int16;
			}
		}

		public TypeReference UInt16 {
			get {
				if (type_uint16 == null)
					type_uint16 = LookupValueType (Constants.UInt16, ElementType.U2);

				return type_uint16;
			}
		}

		public TypeReference Int32 {
			get {
				if (type_int32 == null)
					type_int32 = LookupValueType (Constants.Int32, ElementType.I4);

				return type_int32;
			}
		}

		public TypeReference UInt32 {
			get {
				if (type_uint32 == null)
					type_uint32 = LookupValueType (Constants.UInt32, ElementType.U4);

				return type_uint32;
			}
		}

		public TypeReference Int64 {
			get {
				if (type_int64 == null)
					type_int64 = LookupValueType (Constants.Int64, ElementType.I8);

				return type_int64;
			}
		}

		public TypeReference UInt64 {
			get {
				if (type_uint64 == null)
					type_uint64 = LookupValueType (Constants.UInt64, ElementType.U8);

				return type_uint64;
			}
		}

		public TypeReference Single {
			get {
				if (type_single == null)
					type_single = LookupValueType (Constants.Single, ElementType.R4);

				return type_single;
			}
		}

		public TypeReference Double {
			get {
				if (type_double == null)
					type_double = LookupValueType (Constants.Double, ElementType.R8);

				return type_double;
			}
		}

		public TypeReference IntPtr {
			get {
				if (type_intptr == null)
					type_intptr = LookupValueType (Constants.IntPtr, ElementType.I);

				return type_intptr;
			}
		}

		public TypeReference UIntPtr {
			get {
				if (type_uintptr == null)
					type_uintptr = LookupValueType (Constants.UIntPtr, ElementType.U);

				return type_uintptr;
			}
		}

		public TypeReference String {
			get {
				if (type_string == null)
					type_string = LookupType (Constants.String, ElementType.String);

				return type_string;
			}
		}

		public TypeReference TypedReference {
			get {
				if (type_typedref == null)
					type_typedref = LookupValueType (Constants.TypedReference, ElementType.TypedByRef);

				return type_typedref;
			}
		}
	}
}
